Well, there's nothing like posting a really rubbish program to your blog to
find out the
right way to do it.
Firstly, Philipp Kern has come up with another, even shorter version than
his version I posted in the update to my
original Ruby
post, this time blowing me away with the strangeness of explicit casts
in Ruby. Apparently, casting an input stream to an array produces an array
with one-line-per-element, as in this example:
STDIN.to_a.sort_by rand .each l puts l
As Philipp warns, however: "Ruby is full of fun and power; one must watch
out that the scripts remain readable". For a fairly toy example, this
solution is still fairly readable, however I can see hair growing on this
thing if your program grows large.
Next, Pierre-Charles David showed his qualifications for becoming a
co-author of the next edition of the Pickaxe (AKA "Programming Ruby: The
Pragmatic Programmers' Guide"), with his example and explanation. He's
kindly given permission for me to reproduce his e-mail in it's entireity,
since it's too good to chop up:
Here is the shortest and most readable version of your "randomize stdin"
script I can think of:
#!/usr/bin/env ruby
print ARGF.readlines.sort_by rand
ARGF is a special variable representing either stdin or, in the
case where file names are passed on the command line, the content of these
files (think of Perl's <>). ARGF behaves like an IO object
(although I think it is not one). In particular, it responds to the
#readlines method which splits the receiver's content into lines
and returns an array. In other words,
lines = ARGF.readlines
is the idiomatic way of doing
lines = []
while line = gets() do
lines << line
end
in your example.
The next part (sort_by) uses a new form of the sort method, which I think
is not documented in the Pickaxe. It implements the Schwartzian
transform technique. In essence, a random number is associated to each
element of the array, which are then sorted based on this index.
As a general style remark, note that although Ruby supports "while"
and "for x in y" loops, they are rarely used, in favor of iterators
and/or higher order methods. For example, to gather all the input
lines in an array without using readlines, one might write
lines = Array.new
ARGF.each_line l lines << l
or
lines = ARGF.inject([]) lines, l lines << l
Hope it helps, and welcome to Ruby!
Pierre, your explanation definitely helped, and I hope it helps others too.
The third person to help me out is
Decklin Foster, through a
post
in his blog. What particularly caught me about his solution was the
neat demonstration of another feature of Ruby which I think I will
really learn to love: the ability to dynamically add methods to an
existing class. "Aiee!" I hear all the purists scream -- and I can
see the potential for severe misuse, too. But, since every good idea seems
to get deeply perverted, we may as well have some real useful features to
pervert while we're at it.
Decklin has the clever idea of adding methods to the
Array class
that shuffle the elements of the array in a random fashion. His code
is beautiful in it's simplicity:
class Array
def swap!(a, b)
self[a], self[b] = self[b], self[a]
self
end
def shuffle!
each_index do i
j = i + rand(length - i)
swap!(i, j)
end
self
end
end
He then goes on to rewrite my bumbling example in probably the most
comprehensible manner I've yet seen:
STDIN.readlines.shuffle!.each line puts line
Decklin writes, "Now that is cool.". I couldn't have put it better myself.
My thanks to Philipp, Pierre-Charles, and Decklin, for helping me along the
Ruby path.
So, two new Ruby Rules for me to live by:
-
Whenever you reach for a while, first take a good look around for any
iterator methods which might apply.
-
Never dismiss the power of defining your own methods on an existing
class if it will improve the comprehension of your program.
Hmm, is it possible that Ruby is nothing more than Functional Programming
by Stealth? Aiee!